"""
svn2cq.__init__
"""

#===============================================================================
# Imports
#===============================================================================

import os
import sys
import time
import subprocess
import cPickle as pickle
import cStringIO as StringIO

import svn.fs
import svn.core
import svn.repos
import svn.delta

from itertools import chain
from functools import wraps

#===============================================================================
# Globals
#===============================================================================
__rcsid__ = '$Id: __init__.py 11 2008-11-04 17:39:34Z trent.p.nelson $'
__rcsurl__ = '$URL: http://svn2cq.googlecode.com/svn/trunk/src/svn2cq/__init__.py $'
__copyright__ = 'Copyright 2008 OnResolve Ltd'

SEPARATOR = '=' * 78

#===============================================================================
# Decorators
#===============================================================================
def cache(f):
    @wraps(f)
    def newf(*_args, **_kwds):
        self = _args[0]
        cache = self.__cache__.setdefault(f.func_name, dict())
        id = '%s,%s' % (repr(_args), repr(_kwds))
        if id not in cache:
            cache[id] = f(*_args, **_kwds)
        return cache[id]
    return newf

#===============================================================================
# Constants
#=============================================================================== 
class Constant(dict):
    def __init__(self):
        items = self.__class__.__dict__.items()
        for (key, value) in filter(lambda t: t[0][:2] != '__', items):
            self[value] = key
    def __getattr__(self, name):
        return self.__getitem__(name)
    def __setattr__(self, name, value):
        return self.__setitem__(name, value)           

class _RevisionType(Constant):
    Lock           = 1
    Unlock         = 2
    Commit         = 3
    PropertyChange = 4
RevisionType = _RevisionType()

class _ChangeType(Constant):
    Added    = 1
    Copied   = 2
    Removed  = 3
    Modified = 4
ChangeType = _ChangeType()

#===============================================================================
# Base Classes
#=============================================================================== 

class BaseObject(object):
    def __init__(self, **kwds):
        if kwds:
            self.__setstate__(kwds)
        
    def __setstate__(self, kwds):
        for (key, value) in kwds.items():
            setattr(self, key, value)
            
    def _slots(self):
        slots = list()
        for cls in self.__class__.mro():
            if hasattr(cls, '__slots__'):
                for slot in cls.__slots__:
                    if not slot.startswith('_'):
                        slots.append(slot)
        return slots            
            
    def __getstate__(self):
        return dict((k, getattr(self, k)) for k in self._slots())
    
class Change(BaseObject):
    __slots__ = (
        'type',
        'diff',
        'path',
        'is_dir',
        'base_rev',
        'base_path',
        'text_changed',
        'props_changed',
    )
    def __str__(self):
        if not self.diff:
            return ''
        
        copied = ''
        if self.type == ChangeType.Copied:
            copied = ' (from r%d, %s)' % (self.base_rev, self.base_path)
        
        return '%s: %s%s\n%s\n%s' % (
            ChangeType[self.type],
            self.path,
            copied,
            SEPARATOR,
            self.diff
        )

class ChangeSet(BaseObject):
    __slots__ = (
        'added',
        'copied',
        'removed',
        'modified',
    )
    def __iter__(self):
        return chain(self.added, self.copied, self.removed, self.modified)
    
    def __str__(self):
        s = StringIO.StringIO()
        for changeType in ChangeType:
            desc = ChangeType[changeType]
            changes = getattr(self, desc.lower())
            if not changes:
                continue
            
            s.write('\n%s:\n' % desc)
            for c in changes:
                is_dir = '/' if c.is_dir else ''
                if c.props_changed:
                    if c.text_changed:
                        props = '   (contents, props changed)'
                    else:
                        props = '   (props changed)'
                else:
                    props = ''
                    
                s.write('   %s%s%s\n' % (c.path, is_dir, props))
                if changeType == ChangeType.Copied:
                    if is_dir:
                        text = ''
                    elif c.text_changed:
                        text = ', changed'
                    else:
                        text = ' unchanged'
                    s.write('      - copied%s from r%d, %s%s\n' % \
                            (text, c.base_rev, c.base_path, is_dir))
        s.seek(0)
        return s.read()
    
class Repository(BaseObject):
    __slots__ = ('path',)

class Revision(BaseObject):
    __slots__ = (
        'rev',
        'log',
        'diff',
        'type',
        'paths',
        'author',
        'date_time',
        'repo_path',
        'change_set',
    )
    def __str__(self):
        return 'Repo: %s\nAuthor: %s\nDate: %s\nRevision: ' \
                '%d\n\nLog:\n%s\n%s\n%s' % (
            os.path.basename(self.repo_path),
            self.author,
            self.date_time,
            self.rev,
            self.log,
            self.change_set,
            self.diff
        )

#===============================================================================
# svn-aware Classes
#=============================================================================== 
    
class SvnChange(Change):
    def __init__(self, rev, *args, **kwds):
        Change.__init__(self, *args, **kwds)
        
        if self.is_dir:
            self.diff = ''
            return
        
        singular = True

        if self.type == ChangeType.Added:
            diff = svn.fs.FileDiff(None, None, rev._root, self.path)
            src_label = '(empty file)'
            dst_label = '%s\t%s' % (self.path, rev.date_time)
        
        elif self.type == ChangeType.Copied:
            diff = svn.fs.FileDiff(None, None, rev._root, self.path)
            src_label = '(empty file)'
            dst_label = '%s\t%s' % (self.path, rev.date_time)

        elif self.type == ChangeType.Removed:
            diff = svn.fs.FileDiff(rev.getRoot(self.base_rev),
                                   self.base_path,
                                   None, None)
            src_label = '%s\t%s' % (self.base_path, rev.date_time)
            dst_label = '(empty file)'
            
        elif self.type == ChangeType.Modified:
            diff = svn.fs.FileDiff(rev.getRoot(self.base_rev),
                                   self.base_path,
                                   rev._root,
                                   self.path)
            src_label = self.base_path + '\t(original)'
            dst_label = '%s\t%s' % (self.path, rev.date_time)
            singular = False
            
        else:
            raise ValueError("unknown change type")
        
        if diff.either_binary():
            suffix = '' if singular else 's'
            self.diff = 'Binary file%s.  No diff available.' % suffix
            return

        try:
            buf = StringIO.StringIO()
            src, dst = diff.get_files()
            exe = getDiffExe()
            args = (exe, '-u', '-L', src_label, '-L', dst_label, src, dst)
            p = subprocess.Popen(args, stdout=subprocess.PIPE)
            while 1:
                line = p.stdout.readline()
                if not line:
                    break
                buf.write(line)
            buf.seek(0)
            self.diff = buf.read()
        finally:
            try:
                if os.path.exists(src):
                    os.remove(src)
                if os.path.exists(dst):
                    os.remove(dst)
            except:
                pass
            
class SvnChangeSet(ChangeSet):
    def __init__(self, rev):
        self.added = list()
        self.copied = list()
        self.removed = list()
        self.modified = list()
        
        for (path, change) in rev._changes:
            is_dir = change.item_kind == svn.core.svn_node_dir
            changeType = getChangeType(change)
            c = SvnChange(rev,
                          path          = path,
                          type          = changeType,
                          is_dir        = is_dir,
                          base_rev      = change.base_rev,
                          base_path     = change.base_path,
                          text_changed  = change.text_changed,
                          props_changed = change.prop_changes)
            
            getattr(self, ChangeType[changeType].lower()).append(c)
            
class SvnRepository(Repository):
    __slots__ = (
        '_fs',
        '_repo',
    )
    def __init__(self, path):
        self.path = path
        self._repo = svn.repos.open(path)
        self._fs = svn.repos.fs(self._repo)

    def getRoot(self, rev):
        return svn.fs.revision_root(self._fs, rev)
    
    def getProperty(self, rev, name):
        return svn.fs.revision_prop(self._fs, rev, name)
    
    def getPool(self):
        return self._parent_pool
    
    def __getattr__(self, attr):
        return getattr(self._repo, attr)

class SvnRevision(Revision):
    __slots__ = (
        '_repo',
        '_root',
        '__cache__',
    )

    def __init__(self, repo_path, rev):
        self.__cache__ = dict()
        self.rev = rev
        self.repo_path = repo_path
        
        self._repo = SvnRepository(repo_path)
        self._root = self.getRoot(rev)
        self.author = self.getProperty(svn.core.SVN_PROP_REVISION_AUTHOR)
        
        self.date_time = time.ctime(
            svn.core.secs_from_timestr(
                self.getProperty(svn.core.SVN_PROP_REVISION_DATE),
                self._repo.getPool()
            )
        )
        
        self._load()

    @cache
    def getRoot(self, rev):
        return self._repo.getRoot(rev)

    def getProperty(self, name):
        return self._repo.getProperty(self.rev, name)

    def _load(self):
        raise NotImplementedError
        
class SvnCommit(SvnRevision):
    __slots__ = (
        '_changes',
        '_dirlist',
    )

    def __init__(self, repo, rev):
        SvnRevision.__init__(self, repo, rev)
        self.log = self.getProperty(svn.core.SVN_PROP_REVISION_LOG)

    def _load(self):
        """
        """
        self.type = RevisionType.Commit
        fs = self._repo._fs
        root = self._root
        editor = svn.repos.ChangeCollector(fs, root)
        ptr, baton = svn.delta.make_editor(editor)
        svn.repos.replay(root, ptr, baton)

        self._changes = editor.get_changes().items()
        self._dirlist = getChangedDirs(self._changes)
        
        common, dirs = getCommonDirs(self._dirlist)
        dirs.sort()
        dirs = ' '.join(dirs)
        if common:
            self.paths = '%s: %s' % (common, dirs)
        else:
            self.paths = dirs
            
        self.change_set = SvnChangeSet(self)
        self.diff = '\n'.join([ str(change) for change in self.change_set ])
        

#===============================================================================
# Methods
#===============================================================================
def load(filename):
    return pickle.load(open(filename, 'rb'))

def save(obj, filename):
    f = open(filename, 'wb')
    pickle.dump(obj, f)
    f.close()

def joinPath(*args):
    return os.path.normpath(os.path.join(*args))        
        
def getDiffExe():
    return os.path.abspath(joinPath(os.path.dirname(__file__), 'diff.exe'))
        
def getRepository(root):
    return SvnRepository(root)

def getChangedDirs(changes):
    dirs = dict()
    for path, change in changes:
        if change.item_kind == svn.core.svn_node_dir:
            dirs[path] = None
        else:
            idx = path.rfind('/')
            if idx == -1:
                dirs[''] = None
            else:
                dirs[path[:idx]] = None
    return dirs.keys()

def getCommonDirs(dirlist):
    """
    Figure out the common portion/parent (commondir) of all the paths
    in DIRLIST and return a tuple consisting of commondir, dirlist.  If
    a commondir is found, the dirlist returned is rooted in that
    commondir.  If no commondir is found, dirlist is returned unchanged,
    and commondir is the empty string.
    """
    if len(dirlist) == 1 or '/' in dirlist:
        commondir = ''
        newdirs = dirlist
    else:
        common = dirlist[0].split('/')
        for j in range(1, len(dirlist)):
            d = dirlist[j]
            parts = d.split('/')
            for i in range(len(common)):
                if i == len(parts) or common[i] != parts[i]:
                    del common[i:]
                    break
        commondir = '/'.join(common)
        if commondir:
            # strip the common portion from each directory
            l = len(commondir) + 1
            newdirs = [ ]
            for d in dirlist:
                if d == commondir:
                    newdirs.append('.')
                else:
                    newdirs.append(d[l:])
        else:
            # nothing in common, so reset the list of directories
            newdirs = dirlist

    return commondir, newdirs

def getChangeType(change):
    if change.added and not change.base_path:
        return ChangeType.Added
    elif change.added and change.base_path:
        return ChangeType.Copied
    elif change.path is None:
        return ChangeType.Removed
    elif not change.added and change.path is not None:
        return ChangeType.Modified
    else:
        raise ValueError("could not discern change type")
